package tink.macro;

@:macro class Switch {
	/**
	 * Allows making a type switch.
	 * 
	 * @param	e
	 * @return
	 * 
	 * @usage
	 * Usage 1: Single Identifier
	 * ==========================
	 * 
	 * This type switch
	 * 
	 *	Switch.type(x, switch (someExpression) {
	 *		case Int: x;
	 *		case Float: Std.int(x);
	 *		case String: Std.parseInt(x);
	 *		default: throw 'cannot convert '+x+' to Int';
	 *	});
	 * 
	 * Will be transformed to
	 * 
	 *	{
	 *		var x : Dynamic = someExpression;
	 *		if (Std.is(x, Int)) {
	 *			var x = cast(x, Int);
	 *			x;
	 *		}
	 *		else if (Std.is(x, Float)) {
	 *			var x = cast(x, Float);
	 *			Std.int(x);
	 *		}
	 *		else if (Std.is(x, String)) {
	 *			var x = cast(x, String);
	 *			Std.parseInt(x);
	 *		}
	 *		else {
	 *			throw "cannot convert " + x + " to Int";
	 *		}
	 *	}
	 *  
	 * Usage 2: Individual Identifiers
	 * ===============================
	 * 
	 * If for some reason it makes sense to refer to the inspected expression by different identifiers 
	 * depending on their type, this syntax allows naming them individually:
	 * 
	 *	Switch.type(_, switch (someExpression) {
	 *		case i, Int: i;
	 *		case f, Float: Std.int(f);
	 *		case s, String: Std.parseInt(s);
	 *		default: throw 'cannot convert ' + _ + ' to Int';
	 *	});
	 */
	public static function type(ident, branches) {
		return new TypeSwitcher().transform(ident, branches);		
	}
}
#if macro
	import haxe.macro.Context;
	import haxe.macro.Expr;
	class TypeSwitcher extends Helper {
		static var ANY_PARAM = TPType(TPath( { name: 'Dynamic', pack: [], params:[], sub: null } ));
		public function new();
		function asType(e:Expr) {
			try {//not particularly beautiful - but it's bug compatible with haXe behaviour for module subdeclarations
				return
					switch (Context.typeof(e)) {
						case TType(t, params):
							var t = t.get();
							if (StringTools.startsWith(t.name, '#')) {
								var name = t.name.substr(1);
								switch (Context.getType(t.pack.concat([name]).join('.'))) {
									case TInst(cl, args):
										var params = [];
										for (i in 0...args.length)
											params.push(ANY_PARAM);
										ComplexType.TPath( { name: name, pack: t.pack, params:params, sub: null } );
									default:
										error('physical type expected', e.pos);
								}
							}
							else 
								error('physical type expected', e.pos);
						default:
							error('physical type expected', e.pos);
					}				
			}
			catch (m:Dynamic) {
				return error(m, e.pos);
			}
		}
		public function transform(ident:Expr, branches:Expr) {
			var name = getIdent(ident);
			switch (branches.expr) {
				case ESwitch(e, cases, def):
					var tail = def;
					var value = this.ident(name);
					cases = cases.copy();
					cases.reverse();
					for (branch in cases) {
						var local = 
							if (name == '_') {
								this.getIdent(branch.values.shift());
							}
							else {
								name;
							}
						if (branch.values.length > 1) 
							error('only one type per case allowed', branch.values[1].pos);
						else {
							var typ = branch.values[0];
							tail = Magic.AST(
								if (Std.is(eval__value, eval__typ)) {
									var eval__name = eval(expr(ECast(value, asType(typ))));
									eval(branch.expr);
								}
								else 
									eval__tail
							);
						}
					}
					return Magic.AST({
						var eval__name:Dynamic = eval__e;
						eval__tail;
					});
				default:
					error('expected switch statement', branches.pos);						
			}
			return null;
		}
	}	
#end